<?php

namespace sys;

use app\admin\model\Config;
use app\model\Version;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use think\Db;
use think\facade\Env;
use util\Logger;

class Update
{
    /**
     * 更新代码
     * @param string $code_url 代码远程地址
     * @param string $sql_url SQL远程地址
     * @return true
     * @throws \Exception
     */
    public function update(string $code_url = '', string $sql_url = '')
    {
        try {
            // 定义本地存放代码的目录
            $localCodeDir = Env::get('root_path') . "/update/" . date('Ymd') . '/';
            // 定义备份目录
            $backupDir = Env::get('root_path') . '/backup/' . date('Ymd') . '/';
            // 创建备份目录
            if (!is_dir($localCodeDir)) {
                mkdir($localCodeDir, 0755, true);
            }
            if (!is_dir($backupDir)) {
                mkdir($backupDir, 0755, true);
            }
            $zipFile = $extractDir = $sqlFile = '';
            if ($code_url) {
                // 备份当前代码
//            $this->backupCode($backupDir);
                // 下载远程代码
                $zipFile  = $localCodeDir . 'latest_code.zip';
                $file_res = $this->downloadRemoteFile($code_url, $zipFile);
                if (!$file_res) {
                    throw new \Exception('下载代码文件失败');
                }
                // 解压代码
                $extractDir = $localCodeDir . 'extracted/';
                $this->unzipCode($zipFile, $extractDir);
                // 替换旧代码
                $this->replaceCode($extractDir);
            }
            if ($sql_url) {
                // 执行 SQL 文件
                $sqlFile = $localCodeDir . 'update.sql';
                $sql_res = $this->downloadRemoteFile($sql_url, $sqlFile);
                if (!$sql_res) {
                    throw new \Exception('下载SQL文件失败');
                }
                $this->executeSqlFile($sqlFile, Db::connect());
            }
            // 清理临时文件
            if ($zipFile && $extractDir) {
                $this->cleanUp($zipFile, $extractDir);
            }
            if ($sqlFile) {
                $this->cleanUp($sqlFile, $extractDir);
            }
            return true;
        } catch (\Exception $e) {
            Logger::error($e->getMessage());
            throw new \Exception($e->getMessage());
        }
    }

    /**
     * 备份当前代码
     * @param $backupDir
     * @return void
     * @throws \Exception
     */
    public function backupCode($backupDir)
    {
        $rootPath   = Env::get('root_path');
        $backupFile = $backupDir . 'backup_' . date('YmdHis') . '.zip';

        // 使用 ZipArchive 创建备份
        $zip = new \ZipArchive();
        if ($zip->open($backupFile, \ZipArchive::CREATE | \ZipArchive::OVERWRITE)) {
            $this->addDirectoryToZip($zip, $rootPath, '');
            $zip->close();
        } else {
            throw new \Exception('无法创建备份文件');
        }
    }

    /**
     * 递归添加目录到 ZipArchive
     * @param $zip
     * @param $dir
     * @param $base
     * @return void
     */
    public function addDirectoryToZip($zip, $dir, $base)
    {
        $files = scandir($dir);
        foreach ($files as $file) {
            if ($file != '.' && $file != '..') {
                $filePath  = $dir . '/' . $file;
                $localPath = $base . '/' . $file;

                if (is_dir($filePath)) {
                    $this->addDirectoryToZip($zip, $filePath, $localPath);
                } else {
                    $zip->addFile($filePath, $localPath);
                }
            }
        }
    }

    /**
     * 解压代码并过滤掉顶级目录
     * @param string $zipFile ZIP 文件路径
     * @param string $extractDir 解压目标目录
     * @throws \Exception 如果解压失败则抛出异常
     */
    public function unzipCode(string $zipFile, string $extractDir)
    {
        if (!file_exists($zipFile)) {
            throw new \Exception("ZIP 文件不存在: {$zipFile}");
        }
        $zip = new \ZipArchive();
        if ($zip->open($zipFile) !== true) {
            throw new \Exception("无法打开 ZIP 文件: {$zipFile}");
        }
        // 创建解压目标目录
        if (!is_dir($extractDir)) {
            mkdir($extractDir, 0755, true);
        }
        // 获取 ZIP 文件中的顶级目录名称
        $topDir = '';
        for ($i = 0; $i < $zip->numFiles; $i++) {
            $entryName = $zip->getNameIndex($i);
            if (str_contains($entryName, '/')) {
                $topDir = explode('/', $entryName)[0];
                break;
            }
        }

        if (empty($topDir)) {
            throw new \Exception("ZIP 文件中未找到顶级目录");
        }

        // 解压并过滤掉顶级目录
        for ($i = 0; $i < $zip->numFiles; $i++) {
            $entryName = $zip->getNameIndex($i);
            // 跳过顶级目录
            if (!str_starts_with($entryName, $topDir . '/')) {
                continue;
            }
            // 计算目标路径
            $relativePath = substr($entryName, strlen($topDir . '/'));
            $targetPath   = $extractDir . '/' . $relativePath;
            // 如果是目录，则创建目录
            if (str_ends_with($entryName, '/')) {
                if (!is_dir($targetPath)) {
                    mkdir($targetPath, 0755, true);
                }
            } else {
                // 如果是文件，则解压文件
                $fileContent = $zip->getFromIndex($i);
                file_put_contents($targetPath, $fileContent);
            }
        }

        $zip->close();
    }

    /**
     * 替换旧代码
     * @param string $extractDir 解压后的目录
     * @return void
     */
    public function replaceCode(string $extractDir)
    {
        $rootPath = Env::get('root_path');
        // 遍历解压后的目录，替换旧代码
        $this->copyDirectory($extractDir, $rootPath);
    }

    /**
     * 递归复制目录
     * @param $src
     * @param $dst
     * @return void
     */
    public function copyDirectory($src, $dst)
    {
        $dir = opendir($src);
        @mkdir($dst, 0755, true);
        while (false !== ($file = readdir($dir))) {
            if ($file != '.' && $file != '..') {
                if (is_dir($src . '/' . $file)) {
                    $this->copyDirectory($src . '/' . $file, $dst . '/' . $file);
                } else {
                    copy($src . '/' . $file, $dst . '/' . $file);
                }
            }
        }
        closedir($dir);
    }

    /**
     * 执行 SQL 文件
     * @param string $sqlFile SQL 文件路径
     * @param \think\db\ConnectionInterface $db 数据库连接实例
     * @return array 返回执行结果（成功或失败信息）
     */
    public function executeSqlFile($sqlFile, $db)
    {
        if (!file_exists($sqlFile)) {
            return ['code' => -1, 'msg' => 'SQL 文件不存在'];
        }

        // 读取 SQL 文件内容
        $sqlContent = file_get_contents($sqlFile);
        if (empty($sqlContent)) {
            return ['code' => -1, 'msg' => 'SQL 文件内容为空'];
        }

        // 按分号分割 SQL 语句
        $sqlStatements      = explode(';', $sqlContent);
        $executedStatements = 0;
        $errors             = [];

        foreach ($sqlStatements as $sql) {
            // 去除空白字符
            $sql = trim($sql);
            if (empty($sql)) {
                continue;
            }

            try {
                // 执行 SQL 语句
                $db->execute($sql);
                $executedStatements++;
            } catch (\Exception $e) {
                // 捕获异常并记录错误
                $errors[] = [
                    'sql'   => $sql,
                    'error' => $e->getMessage(),
                ];
            }
        }

        if (!empty($errors)) {
            return [
                'code'   => false,
                'msg'    => '部分 SQL 语句执行失败',
                'errors' => $errors,
            ];
        }

        return [
            'code' => true,
            'msg'  => "成功执行 {$executedStatements} 条 SQL 语句",
        ];
    }

    /**
     * 清理临时文件
     * @param $zipFile
     * @param $extractDir
     * @return void
     */
    public function cleanUp($zipFile, $extractDir)
    {
        unlink($zipFile);
        $this->deleteDirectory($extractDir);
    }

    /**
     * 递归删除目录
     * @param $dir
     * @return void
     */
    public function deleteDirectory($dir)
    {
        if (is_dir($dir)) {
            $objects = scandir($dir);
            foreach ($objects as $object) {
                if ($object != '.' && $object != '..') {
                    if (is_dir($dir . '/' . $object)) {
                        $this->deleteDirectory($dir . '/' . $object);
                    } else {
                        unlink($dir . '/' . $object);
                    }
                }
            }
            rmdir($dir);
        }
    }

    /**
     * 下载文件
     * @param string $remoteUrl 远程地址
     * @param string $localFile 本地文件路径
     * @return bool
     */
    public function downloadRemoteFile(string $remoteUrl, string $localFile)
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $remoteUrl);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
        $data = curl_exec($ch);
        curl_close($ch);

        if (file_put_contents($localFile, $data)) {
            return true;
        } else {
            \util\Logger::error('下载远程文件失败：' . $remoteUrl);
            return false;
        }
    }
}
